﻿using System;
using System.Console;
using Nemerle.Utility;
using Nemerle.Collections;

using System.IO;
using System.Text;
using SCG = System.Collections.Generic;
using XML = System.Xml;

namespace Nemerle.Xml.Dom
{
  using Node;
  using DocumentElement;

  public abstract class XmlElement
  {
    public abstract Transform(transform : XmlElement -> XmlElement) : XmlElement;        
    public override ToString() : string { XmlPrinter().Print(this) }
  }

  public module Utils
  {
    public SkipSpaces(this lst : list[Node]) : SCG.IEnumerable[Node]
    {
      lst.FilterLazy(x => !(x is Whitespace))
    }
    
    public TryForAll2[T1, T2](this lst1: SCG.IEnumerable[T1], lst2: SCG.IEnumerable[T2], predicate : T1 * T2 -> bool) : bool 
    {
      try
      {
        lst1.ForAll2 (lst2, predicate)
      }
      catch
      {
        | _ is ArgumentException => false
      }
    }
  }

  public variant Node : XmlElement
  {
    | Element { name : string; attrs : list[string * string]; nodes : list[Node]; }
    | Text { value : string; }
    | Whitespace { value : string; }    
    
    public override Transform(transform: XmlElement -> XmlElement ) : XmlElement
    {
      match (this)
      {        
        | Text(_) as t => transform(t)
        | Whitespace(_) as w => transform(w)
        | Element(_, _, _) as e =>
        
          def res = transform(e);
          match (res)
          { 
            | Element(name, attrs, nodes) => 
              def result = nodes.Map( _.Transform(transform) ).FilterLazy( x => x != null ).Map( x => x :> Node );
              if (result.TryForAll2(nodes, object.ReferenceEquals))
                res
              else Element(name, attrs, result)         
              
            | _ => res
          }          
      }
    }
    
    public override GetHashCode() : int
    {
      match (this)
      {
        | Text(text) => text.GetHashCode()
        | Whitespace(text) => text.GetHashCode()
        | Element(name, _, _) => name.GetHashCode()
      }
    }
    
    public override Equals(o: object) : bool
    {
      if (object.ReferenceEquals(this, o)) true      
      else             
        match (this, o)
        {
          | (Element(name, attrs, nodes), Element(name1, attrs1, nodes1)) => 
                                                                     
                name == name1                                                                          
                && attrs.TryForAll2(attrs1, fun ((n, v),(n1, v1)) { n == n1 && v == v1 })
                && nodes.SkipSpaces().TryForAll2(nodes1.SkipSpaces(), (x, y) => x.Equals(y) )                                                                          
                                                                          
          | (Text(l), Text(r)) => l == r
          | (Whitespace(_), Whitespace(_)) => true
          | (_, _) => false
        }        
    }
  }

  public variant DocumentElement : XmlElement
  {
    | Document { instrs : list[ProcessingInstruction]; nodes : list[Node]; }
    | ProcessingInstruction { name : string; value : string; }
     
    public override Transform(transform : XmlElement -> XmlElement) : XmlElement
    {
      match (this)
      { 
        | Document(pi, nodes) =>            
          def newNodes = nodes.Map( _.Transform(transform) ).FilterLazy( x => x != null ).Map( x => x :> Node );
          def newPI = pi.Map( transform ).FilterLazy( x => x != null ).Map( x => x :> ProcessingInstruction );
          if (newNodes.TryForAll2(nodes, object.ReferenceEquals) && newPI.TryForAll2(pi, object.ReferenceEquals))
            this          
          else Document(newPI, newNodes)
        | _ => transform(this)
      }
    }
    
    public override GetHashCode() : int
    {
      match (this)
      {
        | ProcessingInstruction(_, value) => value.GetHashCode()
        | Document([], []) => base.GetHashCode()
        | Document(_, head :: _) => head.GetHashCode()        
        | Document(head :: _, []) => head.GetHashCode()        
      }
    }
    
    public override Equals(o: object) : bool
    {
      if (object.ReferenceEquals(this, o)) true      
      else             
        match (this, o)
        {
          | (Document(leftPI, leftNodes), Document(rightPI, rightNodes)) =>
              leftPI.TryForAll2(rightPI, (x, y) => x.Equals(y) )
              && leftNodes.SkipSpaces().TryForAll2(rightNodes.SkipSpaces(), (x, y) => x.Equals(y) )
        
          | (ProcessingInstruction(name, value), ProcessingInstruction(name1, value1)) =>
                                                          name == name1 && value == value1
          | (_, _) => false
        }        
    }
  }

  [StringTemplate.StringTemplateGroup]
  public class XmlPrinter
  {
    private RemoveWhitespaces(nodes : list[Node]) : list[Node]
    {
      nodes.Filter(n => !(n is Whitespace))
    }
    
    public Print(xml : XmlElement) : string
    {   
      | Text(value)                        => Escape(value)
      | Whitespace(_value)                 => ""
      | Element(name, [], [Text(text)])    => PrintTextNodeInline(name, text)
      | Element(name, attrs, [Text(text)]) => PrintTextNodeInline(name, attrs, text)
      | Element(_, [], []) as node         => PrintEmptyNode(node)
      | Element(_, _, [])  as node         => PrintNodeWithoutSubnodes(node)
      | Element(name, attrs, nodes)        => PrintNode(name, attrs, RemoveWhitespaces(nodes))      
      | Document(instrs, nodes)            => PrintDocument(instrs, RemoveWhitespaces(nodes))
      | ProcessingInstruction(name, value) => PrintProcessingInstruction(name, value)        
      | null                                   => ""
      | _                                      => throw ArgumentException("", "xml")    
    }
    
    PrintDocument(instrs : list[ProcessingInstruction], nodes : list[Node]) : string {<#
    ..$(instrs; "\n"; i => PrintProcessingInstruction(i.name, i.value))
    ..$(nodes; "\n"; Print)
    #>}

    PrintProcessingInstruction(name : string, value : string) : string {<#
    <?$name $value?>  
    #>}

    PrintEmptyNode(node : Node.Element) : string {<#<$(node.name)/>#>}
    
    PrintNodeWithoutSubnodes(node : Node.Element) : string {<#
    <$(node.name)$(AddSpaceIfNotEmpty(node.attrs))$(PrintAttrs(node.attrs))/>
    #>}

    PrintNode(name : string, attrs : list[string * string], nodes : list[Node]) : string {<#
    <$name$(AddSpaceIfNotEmpty(attrs))$(PrintAttrs(attrs))>
      ..$(nodes; "\n"; Print)
    </$name>
    #>}
    
    PrintTextNodeInline(name : string, text : string) : string {<#
    <$name>$text</$name>
    #>}
    
    PrintTextNodeInline(name : string, attrs : list[string * string], text : string) : string {<#
    <$name$(AddSpaceIfNotEmpty(attrs))$(PrintAttrs(attrs))>$text</$name>
    #>}

    PrintAttrs(attrs : list[string * string]) : string {<#
    ..$(attrs; " "; PrintAttr)
    #>}
    
    PrintAttr(name : string, value : string) : string {<#
    $name="$(Escape(value))"
    #>}
    
    Escape(value : string) : string { value } // not yet work correctlly
    
    AddSpaceIfNotEmpty[T](lst : list[T]) : string
    {
      if (lst.IsEmpty) "" else " "
    }
  }

  public module DomReader
  {
    public Read(reader : XML.XmlReader, ignoreWhitespace : bool = false) : XmlElement
    {
      def readAttrs(attrs = [])
      { 
        if (reader.MoveToNextAttribute())
          readAttrs( (reader.Name, reader.Value) :: attrs )
        else attrs.Reverse()
      }    
        
      def readPI(instructions = [])
      {
        match(reader.NodeType)
        {
          | XML.XmlNodeType.ProcessingInstruction                      
          | XML.XmlNodeType.XmlDeclaration =>       
                                            def res = ProcessingInstruction(reader.Name, reader.Value) :: instructions;                                            
                                            if (reader.Read()) readPI(res) else res
                        
          | XML.XmlNodeType.Whitespace          
          | XML.XmlNodeType.SignificantWhitespace => _ = reader.Read(); readPI(instructions)        
          | _ => instructions
        }      
      }
      
      def readNodes(nodes = [])
      {
        def reader = reader;
        def nodeType = reader.NodeType;
        def res = match (nodeType)
        {
          | XML.XmlNodeType.Element => def empty = reader.IsEmptyElement;
                                        Element(reader.Name, readAttrs(), 
                                        if (empty) [] 
                                        else { _ = reader.Read(); readNodes()}) :: nodes
                        
          | XML.XmlNodeType.Text => Text(reader.Value) :: nodes
          | XML.XmlNodeType.CDATA => Text(reader.Value) :: nodes
          | XML.XmlNodeType.SignificantWhitespace 
          | XML.XmlNodeType.Whitespace => if (ignoreWhitespace) nodes else Whitespace(reader.Value) :: nodes
          | _ => nodes
        }
        
        if (!reader.Read() || nodeType == XML.XmlNodeType.EndElement) res.Reverse() 
        else readNodes(res)
      }

      if (reader.Read()) Document(readPI().Reverse(), readNodes().Reverse())
      else Document([], [])
    }
  }

  public module DomWriter
  {
    public Write(element: XmlElement, writer: XML.XmlWriter, writeWhitespaces = false) : void
    {
      def writeAttrs(attrs)
      {
        attrs.Iter(writer.WriteAttributeString)
      }
      
      def writePI(instructions)
      {
        | [] => ()
        | head :: tail => writer.WriteProcessingInstruction(head.name, head.value);                      
                          writePI(tail)
      }
    
      def writeNodes(elements)
      {
        unless (elements is [])
        {    
          match (elements.Head)
          {
            | Text(text) => writer.WriteString(text)
            | Whitespace(_text) => if (writeWhitespaces) writer.WriteWhitespace(_text) else ()
            | Element(name, [], []) => writer.WriteElementString(name, "")
            | Element(name, attrs, []) => writer.WriteStartElement(name); writeAttrs(attrs); writer.WriteEndElement()
            | Element(name, [], nodes) => writer.WriteStartElement(name); writeNodes(nodes); writer.WriteFullEndElement()
            | Element(name, attrs, nodes) => writer.WriteStartElement(name);
                                              writeAttrs(attrs);
                                              writeNodes(nodes);
                                              writer.WriteFullEndElement()          
          }
          
          writeNodes(elements.Tail)
        }
      }    
      
      match (element)    
      {
        | Document(pi, nodes) => writePI(pi); writeNodes(nodes)
        | ProcessingInstruction(_, _) as pi => writePI( [pi] )
        | _ => writeNodes( [element :> Node] )
      }
    }
  }

}

using Nemerle.Xml.Dom;
using Nemerle.Xml.Dom.Node;
using Nemerle.Xml.Dom.DocumentElement;

module Program
{
  Main() : void
  {   
    def template = 
      Document([ProcessingInstruction("xml", "version = \"1.0\" encoding=\"utf-8\""),
                ProcessingInstruction("xsl-stylesheet", "version = \"1.0\" encoding=\"utf-8\"")], [
        Element("x:html", [("xmlns:x","somens")], [
          Element("title", [("test-atr5", "test value5")], [Text("lalala\nTitle\nkos")]),
          Element("title2", [], [Text("Title2")]),
          Element("body", [("xmlns", "http://www.nemerle.org")], [
            Element("p", [], [Text("Hellow, world!")]),
            Element("table", [("atr", "attr value")], []),
            Element("order", [("att", "12"), ("br", "55")], [Text("cool")])
          ])
        ])]);
        
    mutable xml = 
      Document([ProcessingInstruction("xml", "version = \"1.0\" encoding=\"utf-8\""),
                ProcessingInstruction("xsl-stylesheet", "version = \"1.0\" encoding=\"utf-8\"")], [
        Element("x:html", [("xmlns:x","somens")], [
          Element("title", [("test-atr5", "test value5")], [Text("lalala\nTitle\nkos")]),
          Element("title2", [], [Text("Title2")]),
          Element("body", [("xmlns", "http://www.nemerle.org")], [
            Element("p", [], [Text("Hellow, world!")]),
            Element("table", [("atr", "attr value")], []),
            Element("order", [("att", "12"), ("br", "55")], [Text("cool")])
          ])
        ])]);

//    WriteLine(xml);    
    
    def transform(el : XmlElement)
    {
      //| Element("body", _, _) as el => Element("body-element", el.attrs, el.nodes);
      //| Element("body-element", _, _) as el => Element("wow", el.attrs, []);      
      //| Element("html", attrs, nodes) => Element("root", attrs + [ ("a", "2"), ("b", "4") ], nodes);      
      | Element("body", _, _) => null
      | Element(name, a, n) => Element(name, a, n)      
      | ProcessingInstruction(_, _) as pi => ProcessingInstruction(pi.name, "")
      | _ => el
    } 
    
//    xml = xml.Transform(transform) :> Document;
    WriteLine(xml);
    
    WriteLine( xml.Transform(transform) );
    WriteLine( xml.Equals(xml.Transform(transform)) );    
    
    WriteLine( xml.Equals(template) );
    
//    WriteLine("\n----DOM----\n");
    
    //def dom = DomReader.Read( XML.XmlTextReader( System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(xml.ToString())) ) );    
    //WriteLine(dom);
    //
    //WriteLine("\n----DomWriter----\n");
    ////
    def writer = XML.XmlTextWriter(Console.Out);   
    writer.Formatting = XML.Formatting.Indented;    
    DomWriter.Write(xml, writer);
    //
    //WriteLine("\n");
    
//    writer.Formatting = XML.Formatting.Indented;
//    DomWriter.Write(DomReader.Read( XML.XmlTextReader("http://www.rsdn.ru/Forum/opml.aspx") ), writer);
    
//    def dom2 = DomReader.Read( XML.XmlTextReader( System.IO.MemoryStream(System.Text.Encoding.UTF8.GetBytes(dom.ToString())) ) );    
//    WriteLine(dom2);
    
    //def xmlDoc = XML.XmlDocument();
    //xmlDoc.Load(XML.XmlTextReader("http://www.rsdn.ru/Forum/opml.aspx"));
    //
//    def dom = DomReader.Read( XML.XmlTextReader("http://www.rsdn.ru/Forum/opml.aspx") );    
//    WriteLine(dom.ToString() == xmlDoc.OuterXml);
//    WriteLine(dom);    

    //def reader = XML.XmlTextReader( MemoryStream(Encoding.UTF8.GetBytes("<?xml version=\"1.0\" encoding=\"utf-8\"?>")) ) ;    
    //while (reader.Read())
    //{
    //  def _v = reader.Value;
    //}
    
    _ = ReadKey();
  }
} 